const node_icon={
    'Fault':["glyphicon","glyphicon-alert"],
    'Feature':["glyphicon","glyphicon-star"],
    'Reason':["glyphicon","glyphicon-star-empty"],
    'Component':["glyphicon","glyphicon-cog"],
    'FeatureGroup':["glyphicon","glyphicon-folder-open"],
    'ReasonGroup':["glyphicon","glyphicon-folder-open"],
    'ComponentGroup':["glyphicon","glyphicon-folder-open"]
};

class Graph{
    constructor(){
        this.nodes=new Map();
        this.edges=[];
        this.matchedNodes=[];
    }

    static merge(...gs){
        let g = new Graph();
        g.nodes = new Map(
            gs.map(gg=>gg.nodes.keys()).reduce((x,y)=>x.concat(y),[]),
            gs.map(gg=>gg.nodes.values()).reduce((x,y)=>x.concat(y),[])
            );
        g.edges = gs.map(gg=>gg.edges).reduce((x,y)=>x.concat(y),[]);
    }

    addNode(node){
        this.nodes.set(node.id,node);
    }
    getNode(id){
        return this.nodes.get(`${id}`);
    }

    getNodeFromList(id,list){
        return [...list].find(n=>n.id == `${id}`);
    }

    addLink(from,to,$state='new'){
        //TODO 需加入“环路”检测！！！
        let edges = this.edges.filter(edge=>`${edge.from}`===`${from}` && `${edge.to}`===`${to}`);
        if(edges.length>0){
            edges.forEach(edge=>edge.$removed = true);
            edges[0].$removed = false;
        }
        else if (this.getNode(`${from}`)==null || this.getNode(`${to}`)==null){
            let newEdge = new Edge(this,from,to,$state);
            newEdge.$removed = true;
            this.edges.push(newEdge);
        } else
            this.edges.push(new Edge(this,from,to,$state));
    }
    getLink(id){
        for (let i=0;i<this.edges.length;i++){
            let eg = this.edges[i];
            if(`${eg.id}`==`${id}`)
                return eg;
        }
    }

    addDBLink(db_from,db_to,$state='raw'){
        let nodesList = [... this.nodes.values()];
        let source = nodesList.filter(node=>node.db_id==db_from)[0];
        let target = nodesList.filter(node=>node.db_id==db_to)[0];
        this.addLink(source.id,target.id,$state);
    }

    getLinkIndex(id){
        for (let i=0;i<this.edges.length;i++){
            let eg = this.edges[i];
            if(`${eg.id}`==`${id}`)
                return i;
        }
        return -1;
    }

    resetLink(edgeId,toNode,callback){
        for (let i=0;i<this.edges.length;i++){
            let eg = this.edges[i];
            if(`${eg.id}`==`${edgeId}`) {
                let neg = new Edge(this,eg.from,toNode.id);
                eg.$removed = true;
                this.addNode(toNode);
                this.edges.push(neg);
                break;
            }
        }
        if(callback != null && callback instanceof Function){
            callback(this);
        }
    }

    toJsonTree(){
        //TODO 转为 bootstrap 所需的树状结构
        const that = this;
        function toTree(nodeId,fromEdge) {
            let sub_edges = [...that.edges].filter(edge=>(!edge.$removed)&&edge.from==nodeId);
            let node = that.getNode(nodeId);
            if(sub_edges.length>0){
                return {
                    text: node.getText(),
                    referId:nodeId,
                    referEdge:fromEdge,
                    icon: node_icon[node.$type],
                    selectable: true,
                    isMatched:node.isMatched||that.matchedNodes.some(nid=>nid=node.id),
                    children:sub_edges.map(edge=>toTree(edge.to,edge.id))
                };
            }else{
                return {
                    text: node.getText(),
                    referId:nodeId,
                    referEdge:fromEdge,
                    icon: node_icon[node.$type],
                    selectable: true,
                    isMatched:node.isMatched||that.matchedNodes.some(nid=>nid=node.id),
                    children:[]
                }
            }
        }

        let nodesList = [... this.nodes.values()];
        let root = nodesList.filter(node=>node.$type=='Fault')[0];
        return toTree(root.id,null);
    }

    toJson(){
        const that = this;
        function exposeJson(node) {
            let json = node.toJson();
            let sub_edges = [...that.edges].filter(edge=>(!edge.$removed)&&edge.from==node.id);
            if(sub_edges.length>0){
                json["children"] = sub_edges.map(edge=>that.getNode(edge.to)).map(nd=>exposeJson(nd));
            }
            return json;
        }

        let fault = this.getRoot();
        return exposeJson(fault);
    }

    getRoot(){
        let nodesList = [... this.nodes.values()];
        let root = nodesList.filter(node=>node.$type=='Fault')[0];
        return root;
    }

    getByType(type) {
        return [... this.nodes.values()].filter(node=>node.$type==type);
    }

    getReasons(){
        return _(this.edges).chain()
            .filter(edge=>!edge.$removed)
            .map(edge=>edge.to)
            .uniq()
            .map(id=>this.getNode(id))
            .filter(node=>node.$type=='Reason')
            .value()
            .sort(MyNode.compare);

        // let nodesList = [... this.nodes.values()].filter(node=>node.$type=='Reason').sort(MyNode.compare);
        // return nodesList;
    }

    getFeatures(){
        return _(this.edges).chain()
            .filter(edge=>!edge.$removed)
            .map(edge=>edge.to)
            .uniq()
            .map(id=>this.getNode(id))
            .filter(node=>node.$type=='Feature')
            .value()
            .sort(MyNode.compare);
        // let nodesList = [... this.nodes.values()].filter(node=>node.$type=='Feature').sort(MyNode.compare);
        // return nodesList;
    }

    getFeatureSize(){
        return this.getFeatures().length;
    }

    getReasonSize(){
        return this.getReasons().length;
    }

    markSubGraph(){
        const that = this;
        let session = neo4jService.getSession();
        function mark(node){
            if (node.$type == 'Feature' || (node.$type == 'FeatureGroup' && node.combineType != 'or')){
                return new Promise(function(resolve, reject){
                    node.$is_sub_graph = true;
                    resolve();
                });
            }else if (node.$type == 'Fault' || (node.$type == 'FeatureGroup' && node.combineType == 'or')){
                return MyNode.nextHop(session,node.db_id).then(nextHop=>{
                    return Promise.all(nextHop.map(nd=>{
                        let t_nd = that.getNode(nd.id);
                        return mark(t_nd);
                    }));
                });
            }
        }
        return mark(that.getRoot());
    }

    //mark reason
    markSubGraph2(){
        const that = this;
        let session = neo4jService.getSession();
        function mark2(node,reasonId){
            if (node.$type == 'Feature' || (node.$type == 'FeatureGroup' && node.combineType != 'or')){
                return new Promise(function(resolve, reject){
                    node.$is_sub_graph2 = true;
                    if (that.getNode(reasonId)!=null){
                        node.$reasons.add(reasonId);
                    }else {
                        console.warn(that,reasonId);
                    }
                    resolve();
                });
            }else if (node.$type == 'Fault' || node.$type == 'ReasonGroup' || node.$type == 'Reason' || (node.$type == 'FeatureGroup' && node.combineType == 'or')){
                return MyNode.nextHop(session,node.db_id).then(nextHop=>{
                    return Promise.all(nextHop.map(nd=>{
                        let t_nd = that.getNode(nd.id);
                        if (node.$type == 'Fault' || node.$type == 'ReasonGroup') {
                            return mark2(t_nd,null);
                        }
                        else if (node.$type == 'Reason'){
                            return mark2(t_nd,node.id);
                        } else if(reasonId!=null){
                            return mark2(t_nd,reasonId);
                        }
                        return Promise.resolve(t_nd);
                    }));
                });
            }
        }
        return mark2(that.getRoot(),null);
    }

    //mark running-issue
    markSubGraph3(){
        const that = this;
        let session = neo4jService.getSession();
        function mark2(node,reasonId){
            if (node.$type == 'Feature' || (node.$type == 'FeatureGroup' && node.combineType != 'or')){
                return new Promise(function(resolve, reject){
                    if (reasonId!=null){
                        node.$is_sub_graph3 = true;
                        node.$issues.add(reasonId);
                    }
                    resolve();
                });
            }else if (node.$type == 'Fault' || node.$type == 'ReasonGroup' || node.$type == 'Reason'
                || node.$type == 'RunningIssue'
                || (node.$type == 'FeatureGroup' && node.combineType == 'or')){
                return MyNode.nextHop(session,node.db_id).then(nextHop=>{
                    return Promise.all(nextHop.map(nd=>{
                        let t_nd = that.getNode(nd.id);
                        if (node.$type == 'Fault' || node.$type == 'ReasonGroup' || node.$type == 'Reason' ) {
                            return mark2(t_nd,null);
                        }
                        else if (node.$type == 'RunningIssue'){
                            return mark2(t_nd,node.id);
                        } else if(reasonId!=null){
                            return mark2(t_nd,reasonId);
                        }
                        return Promise.resolve(t_nd);
                    }));
                });
            }
        }
        return mark2(that.getRoot(),null);
    }

    //mark solution
    markSubGraph4(){
        const that = this;
        let session = neo4jService.getSession();
        function mark2(node,reasonId){
            if (node.$type == 'Feature' || (node.$type == 'FeatureGroup' && node.combineType != 'or')){
                return new Promise(function(resolve, reject){
                    if (reasonId!=null){
                        node.$is_sub_graph4 = true;
                        node.$solutions.add(reasonId);
                    }
                    resolve();
                });
            }else if (node.$type == 'Fault' || node.$type == 'ReasonGroup' || node.$type == 'Reason'
                || node.$type == 'Solution'
                || (node.$type == 'FeatureGroup' && node.combineType == 'or')){
                return MyNode.nextHop(session,node.db_id).then(nextHop=>{
                    return Promise.all(nextHop.map(nd=>{
                        let t_nd = that.getNode(nd.id);
                        if (node.$type == 'Fault' || node.$type == 'ReasonGroup' || node.$type == 'Reason' ) {
                            return mark2(t_nd,null);
                        }
                        else if (node.$type == 'Solution'){
                            return mark2(t_nd,node.id);
                        } else if(reasonId!=null){
                            return mark2(t_nd,reasonId);
                        }
                        return Promise.resolve(t_nd);
                    }));
                });
            }
        }
        return mark2(that.getRoot(),null);
    }

    dropOp(session){
        return new Promise((resolve,reject)=>{
            session.run(`match (m)-[]->(n) 
                with n,count(*) as ct where ct<>0 with collect(id(n)) as ns 
                match (p) where not id(p) in ns 
                and (not p:Fault and not p:Component and not p:Tool and not p:Material and not p:FeatureType)
                return id(p) as pid`)
                .then(result=> {
                    const pids = result.records
                        // .map((record) => record.get(0))
                        .map(r=>r.get("pid").toNumber());
                    let i=0;
                    const dropEachItem = ()=>{
                        if (i>=pids.length){
                            resolve();
                            return;
                        }
                        const pid = pids[i];
                        return new Promise((resolve,reject)=>{
                            session.run(`start n=node(${pid}) match (n)-[r]->() delete r`)
                                .then(()=>{
                                    session.run(`start n=node(${pid}) delete n`)
                                        .then( ()=>{
                                            i++;
                                            return dropEachItem();
                                        })
                                })
                        })
                    };
                    return dropEachItem();
                })
        });
    }

    removeFromDB(callback){
        let root = this.getRoot();
        const fail = function (e) {
            console.error(e);
        };

        let session = neo4jService.getSession();

        session.run(`match (n)-[*]->()-[r]->(m:Component) where id(n)=${root.db_id} delete r `)
            .then(r1=>session.run(`match (n)-[r]->() where id(n)=${root.db_id} delete r delete n`))
            .then(r2=>`match (n) where n:Component or n:Configure with id(n) as x return x union match p=(n:Fault)-[*]->() with nodes(p) as pl unwind pl as xn with id(xn) as x return distinct x`)
            .then(r3=>{
                const ids = r3.records.map((record) => record.get(0));
                return session.run(`with [${ids.join(",")}] as nids match (n)-[r]-() where not id(n) in nids delete r delete n`).then(()=>ids);
            })
            .then(ids=>session.run(`with [${ids.join(",")}] as nids match (n) where not id(n) in nids delete n`))
            .then(()=>this.dropOp(session))
            .then(()=>{
                session.close();
                callback && callback();
            })
            .catch(e=>{
                if(fail != null && fail instanceof Function){
                    fail(e);
                }
            });

        // Neo4jService.doExecute(session,`match (n)-[*]->()-[r]->(m:Component) where id(n)=${root.id} delete r `,(r1)=>{
        //     Neo4jService.doExecute(session,`match (n)-[r]->() where id(n)=${root.id} delete r delete n`,(r2)=>{
        //         Neo4jService.doExecute(session,`match (n) where n:Component or n:Configure with id(n) as x return x union match p=(n:Fault)-[*]->() with nodes(p) as pl unwind pl as xn with id(xn) as x return distinct x`,(r3)=>{
        //             const ids = r3.records.map((record) => record.get(0));
        //             Neo4jService.doExecute(session,`with [${ids.join(",")}] as nids match (n)-[r]-() where not id(n) in nids delete r delete n`,(r4)=>{
        //                 Neo4jService.doExecute(session,`with [${ids.join(",")}] as nids match (n) where not id(n) in nids delete n`,(r5)=>{
        //                     session.close();
        //                     callback && callback();
        //                 },fail);
        //             },fail);
        //         },fail);
        //     },fail);
        // },fail);


    }

    persistClone(success,fail){
        let nds = [...this.nodes.values()];

        nds = nds.filter(nd=>nd.$type!='Component' && nd.$state!='new').map(node=>{
            node.$state = 'new';
            return node;
        });

        this.edges = this.edges.filter(eg=>eg.$state!='new').map(eg=>{
            eg.$state = 'new';
            return eg;
        });

        let rawEntities = nds.filter(nd=>nd.$state!='new').map(node=>`n${node.id}=node(${node.db_id})`);
        let modifiedEntity = nds.filter(nd=>nd.$state!='new').map(node=>{
            let str = '';
            for(let key of Object.keys(node)){
                if(!/^\$\S+/g.test(key) && key!='id'){
                    let jsonStr = angular.toJson(node[key],false);
                    str = `${str} set n${node.db_id}.${key}=${jsonStr} `;
                }
            }
            return str;
        });

        let entities = nds.filter(nd=>nd.$state=='new').map(node=>`(n${node.db_id}${node.toString()}) `);

        let rawEdges = this.edges.filter(eg=>eg.$state!='new'&&eg.$removed).map(eg=>`(n${eg.source().db_id})-[r${eg.id}]->(n${eg.target().db_id})`);
        let newEdges = this.edges.filter(eg=>eg.$state=='new'&&!eg.$removed).map(eg=>`(n${eg.source().db_id})-[r${eg.id}:${eg.linkType()}]->(n${eg.target().db_id})`);
        let deleteEdges = this.edges.filter(eg=>eg.$state!='new'&&eg.$removed).map(eg=>`r${eg.id}`);

        let qstring =``;
        if(rawEntities.length>0)
            qstring = `${qstring} start ${rawEntities.join(',')}`;
        if(rawEdges.length>0)
            qstring = `${qstring} match ${rawEdges.join(',')}`;
        if(entities.length>0)
            qstring = `${qstring} create ${entities.join(',')}`;
        if(newEdges.length>0)
            qstring = `${qstring} create ${newEdges.join(',')}`;
        if(modifiedEntity.length>0)
            qstring = `${qstring} ${modifiedEntity.join(' ')}`;
        if(deleteEdges.length>0)
            qstring = `${qstring} delete ${deleteEdges.join(',')}`;

        let session = neo4jService.getSession();
        let resultPromise = session.run(qstring);
        resultPromise
            .catch(e=> {
                if(fail != null && fail instanceof Function){
                    fail(e);
                }
            })
            .then(result => {
                session.close();
                FaultNode.loadFaults();
                if(success != null && success instanceof Function){
                    success(result);
                }
            });
    }

    static loadGraph(startId,callback){
        return Graph.loadGraphPromise(startId).then(callback);
    }

    static loadGraphPromise(startId){
        return new Promise((resolve,reject)=>{
            resolve({
                g:new Graph(),
                session:neo4jService.getSession()
            });
        })
            .then((context)=>new Promise((resolve,reject)=>{
            const {g,session} = context;
            session.run(`start n=node(${startId}) MATCH (n)-[*]->(a) RETURN a UNION start a=node(${startId}) RETURN a`)
                .then(result=>{
                    result.records
                        .map((record) => record.get(0))
                        .map(node => MyNode.fromNeo4jNode(node))
                        .forEach(node=>g.nodes.set(node.id,node));
                    resolve(context);
                },fail=>{
                    resolve(context);
                });
        }))
            .then((context)=>new Promise((resolve,reject)=>{
            const {g,session} = context;
            session.run(`start n=node(${startId}) match p=(n)-[*]->(a) return relationships(p)[-1]`)
                .then(result=>{
                    result.records
                        .map((record) => record.get(0))
                        .forEach(edge=>g.addDBLink(edge["start"].toString(),edge["end"].toString(),'raw'));
                    resolve(context);
                });
        }))
            .then((context)=>{
                const {g,session} = context;
                session.close();
                return g;
            });
    }

    static loadAllGraph(){
        return new Promise((resolve,reject)=>{
            resolve({
                g:new Graph(),
                session:neo4jService.getSession()
            });
        }).then((context)=>new Promise((resolve,reject)=>{
                const {g,session} = context;
                session.run(`MATCH (n) where not "Configure" in labels(n)  RETURN n`)
                    .then(result=>{
                        result.records
                            .map((record) => record.get(0))
                            .map(node => MyNode.fromNeo4jNode(node))
                            .forEach(node=>{
                                if (!node || !node.id){
                                    console.info(node);
                                }else
                                    g.nodes.set(node.id,node);
                            });
                        resolve(context);
                    },fail=>{
                        resolve(context);
                    });
        })).then((context)=>new Promise((resolve,reject)=>{
            const {g,session} = context;
            session.run(`match ()-[r]->() return r`)
                .then(result=>{
                    result.records
                        .map((record) => record.get(0))
                        .forEach(edge=>g.addDBLink(edge["start"].toString(),edge["end"].toString(),'raw'));
                    resolve(context);
                });
        })) .then((context)=>{
            const {g,session} = context;
            session.close();
            return g;
        });
    }

    markFreedomNode(){
        const that = this;
        function mark(node,parent_type/* or--1 , and--0 */) {
            if (node instanceof FeatureNode){
                if (parent_type=="or"){
                    node.$freedom = true;
                    node.$freelist = [node.id];
                }
            } else {
                for (let edge of that.edges.filter(edge => edge.from == node.id)) {
                    let subnode = that.getNode(edge.to);
                    edge.$freedom = true;
                    mark(subnode,node.combineType);
                }
                let list = [];
                for (let edge of that.edges.filter(edge => edge.from == node.id)) {
                    let subnode = that.getNode(edge.to);
                    list.concat(subnode.$freelist || []);
                }
                node.$freelist = list;
            }
        }
        mark(this.getRoot(),1);
    }

    persist(success,fail){
        this.getRoot()["updateTimestamp"] = new Date().getTime();
        let nds = [...this.nodes.values()];
        // nds.forEach(nd=>nd.persist());
        // this.edges.forEach(eg=>eg.persist());

        let rawEntities = nds.filter(nd=>nd.$state!='new').map(node=>`n${node.id}=node(${node.db_id})`);
        let modifiedEntity = nds.filter(nd=>nd.$state!='new').map(node=>{
            let str = `set n${node.id}.user_id="${node.id}"`;
            for(let key of Object.keys(node)){
                if(key=="updateTimestamp"){
                    str = `${str} set n${node.id}.${key}=${node[key]} `;
                }else
                if(!/^\$\S+/g.test(key) && key!='id' && key!='isMatched' && key != 'unknown' && key!='db_id'){
                    if (key=='matrix' && _.isObject(node[key]) && ! _.isArray(node[key])){
                            node[key]["length"] = (node[key][0]+1) * (node[key][1]+1) +2;
                            node[key] = Array.from(node[key]);
                            node[key] = _(node[key]).chain().map(x=>x instanceof Number?x:parseFloat(x)).value();
                    }

                    let jsonStr = angular.toJson(node[key],false);
                    if (key=="frequency" || key=="reasonFrequency") {
                        jsonStr = angular.toJson(jsonStr);
                    }
                    str = `${str} set n${node.id}.${key}=${jsonStr} `;
                }
            }
            return str;
        });

        let entities = nds.filter(nd=>nd.$state=='new'&&!nd.$removed).map(node=>`(n${node.id}${node.toString()}) `);

        let rawEdges = this.edges.filter(eg=>eg.$state!='new'&&eg.$removed).map(eg=>`(n${eg.from})-[r${eg.id}]->(n${eg.to})`);
        let newEdges = this.edges.filter(eg=>eg.$state=='new'&&!eg.$removed).map(eg=>`(n${eg.from})-[r${eg.id}:${eg.linkType()}]->(n${eg.to})`);
        let deleteEdges = this.edges.filter(eg=>eg.$state!='new'&&eg.$removed).map(eg=>`r${eg.id}`);

        let qstring =``;
        if(rawEntities.length>0)
            qstring = `${qstring} start ${rawEntities.join(',')}`;
        if(rawEdges.length>0)
            qstring = `${qstring} match ${rawEdges.join(',')}`;
        if(entities.length>0)
            qstring = `${qstring} create ${entities.join(',')}`;
        if(newEdges.length>0)
            qstring = `${qstring} create ${newEdges.join(',')}`;
        if(modifiedEntity.length>0)
            qstring = `${qstring} ${modifiedEntity.join(' ')}`;
        if(deleteEdges.length>0)
            qstring = `${qstring} delete ${deleteEdges.join(',')}`;

        let session = neo4jService.getSession();
        let resultPromise = session.run(qstring);
        resultPromise
            .then(()=>this.dropOp(session))
            .catch(e=> {
                if(fail != null && fail instanceof Function){
                    fail(e);
                }
            })
            .then(result => {
                session.close();
                FaultNode.loadFaults();
                if(success != null && success instanceof Function){
                    success(result);
                }
        });
    }
}
